/**
 * Copyright 2011 DFKI GmbH.
 * All Rights Reserved.  Use is subject to license terms.
 *
 * This file is part of MARY TTS.
 *
 * MARY TTS is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, version 3 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package marytts.util;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.StringTokenizer;
import java.util.Vector;

import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;

import marytts.Version;
import marytts.config.LanguageConfig;
import marytts.config.MaryConfig;
import marytts.datatypes.MaryDataType;
import marytts.datatypes.MaryXML;
import marytts.exceptions.MaryConfigurationException;
import marytts.fst.FSTLookup;
import marytts.htsengine.HMMVoice;
import marytts.modules.phonemiser.AllophoneSet;
import marytts.modules.synthesis.Voice;
import marytts.server.Mary;
import marytts.server.MaryProperties;
import marytts.signalproc.effects.AudioEffect;
import marytts.signalproc.effects.AudioEffects;
import marytts.unitselection.UnitSelectionVoice;
import marytts.unitselection.interpolation.InterpolatingVoice;
import marytts.util.data.audio.AudioDestination;
import marytts.util.data.audio.MaryAudioUtils;
import marytts.util.dom.MaryDomUtils;
import marytts.util.string.StringUtils;
import marytts.vocalizations.VocalizationSynthesizer;

import org.w3c.dom.Element;

/**
 * @author marc
 *
 */
public class MaryRuntimeUtils {

	public static void ensureMaryStarted() throws Exception {
		synchronized (MaryConfig.getMainConfig()) {
			if (Mary.currentState() == Mary.STATE_OFF) {
				Mary.startup();
			}
		}
	}

	/**
	 * Instantiate an object by calling one of its constructors.
	 *
	 * @param objectInitInfo
	 *            a string description of the object to instantiate. The objectInitInfo is expected to have one of the following
	 *            forms:
	 *            <ol>
	 *            <li>my.cool.Stuff</li>
	 *            <li>my.cool.Stuff(any,string,args,without,spaces)</li>
	 *            <li>my.cool.Stuff(arguments,$my.special.property,other,args)</li>
	 *            </ol>
	 *            where 'my.special.property' is a property in one of the MARY config files.
	 * @return the newly instantiated object.
	 * @throws MaryConfigurationException
	 *             MaryConfigurationException
	 */
        @SuppressWarnings("unchecked")
	public static Object instantiateObject(String objectInitInfo) throws MaryConfigurationException {
		Object obj = null;
		String[] args = null;
		String className = null;
		try {
			if (objectInitInfo.contains("(")) { // arguments
				int firstOpenBracket = objectInitInfo.indexOf('(');
				className = objectInitInfo.substring(0, firstOpenBracket);
				int lastCloseBracket = objectInitInfo.lastIndexOf(')');
				args = objectInitInfo.substring(firstOpenBracket + 1, lastCloseBracket).split(",");
				for (int i = 0; i < args.length; i++) {
					if (args[i].startsWith("$")) {
						// replace value with content of property named after the $
						args[i] = MaryProperties.getProperty(args[i].substring(1));
					}
					args[i] = args[i].trim();
				}
			} else { // no arguments
				className = objectInitInfo;
			}
			Class<? extends Object> theClass = Class.forName(className).asSubclass(Object.class);
			// Now invoke Constructor with args.length String arguments
			if (args != null) {
				Class<String>[] constructorArgTypes = new Class[args.length];
				Object[] constructorArgs = new Object[args.length];
				for (int i = 0; i < args.length; i++) {
					constructorArgTypes[i] = String.class;
					constructorArgs[i] = args[i];
				}
				Constructor<? extends Object> constructor = (Constructor<? extends Object>) theClass
						.getConstructor(constructorArgTypes);
				obj = constructor.newInstance(constructorArgs);
			} else {
				obj = theClass.getDeclaredConstructor().newInstance();
			}
		} catch (Exception e) {
			// try to make e's message more informative if possible
			throw new MaryConfigurationException("Cannot instantiate object from '" + objectInitInfo + "': "
					+ MaryUtils.getFirstMeaningfulMessage(e), e);
		}
		return obj;
	}

	/**
	 * Verify if the java virtual machine is in a low memory condition. The memory is considered low if less than a specified
	 * value is still available for processing. "Available" memory is calculated using <code>availableMemory()</code>.The
	 * threshold value can be specified as the Mary property mary.lowmemory (in bytes). It defaults to 20000000 bytes.
	 *
	 * @return a boolean indicating whether or not the system is in low memory condition.
	 */
	public static boolean lowMemoryCondition() {
		return MaryUtils.availableMemory() < lowMemoryThreshold();
	}

	/**
	 * Verify if the java virtual machine is in a very low memory condition. The memory is considered very low if less than half a
	 * specified value is still available for processing. "Available" memory is calculated using <code>availableMemory()</code>
	 * .The threshold value can be specified as the Mary property mary.lowmemory (in bytes). It defaults to 20000000 bytes.
	 *
	 * @return a boolean indicating whether or not the system is in very low memory condition.
	 */
	public static boolean veryLowMemoryCondition() {
		return MaryUtils.availableMemory() < lowMemoryThreshold() / 2;
	}

	private static long lowMemoryThreshold() {
		if (lowMemoryThreshold < 0) // not yet initialised
			lowMemoryThreshold = (long) MaryProperties.getInteger("mary.lowmemory", 10000000);
		return lowMemoryThreshold;
	}

	private static long lowMemoryThreshold = -1;

	/**
	 * List the available audio file format types, as a multi-line string. Each line consists of the name of an Audio file format
	 * type, followed by a suffix "_FILE" if the format can be produced as a file, and "_STREAM" if the format can be streamed.
	 *
	 * @return a multi-line string, or an empty string if no audio file types are available.
	 */
	public static String getAudioFileFormatTypes() {
		StringBuilder output = new StringBuilder();
		AudioFileFormat.Type[] audioTypes = AudioSystem.getAudioFileTypes();
		for (int t = 0; t < audioTypes.length; t++) {
			AudioFileFormat.Type audioType = audioTypes[t];
			String typeName = audioType.toString();
			boolean isSupported = true;
			if (typeName.equals("MP3"))
				isSupported = canCreateMP3();
			else if (typeName.equals("Vorbis"))
				isSupported = canCreateOgg();
			audioType = MaryAudioUtils.getAudioFileFormatType(typeName);
			if (audioType == null) {
				isSupported = false;
			}

			if (isSupported && AudioSystem.isFileTypeSupported(audioType)) {
				output.append(typeName).append("_FILE\n");

				if (typeName.equals("MP3") || typeName.equals("Vorbis"))
					output.append(typeName).append("_STREAM\n");
			}
		}
		return output.toString();
	}

	/**
	 * Determine whether conversion to mp3 is possible.
	 *
	 * @return AudioSystem.isConversionSupported(getMP3AudioFormat(), Voice.AF22050)
	 */
	public static boolean canCreateMP3() {
		return AudioSystem.isConversionSupported(getMP3AudioFormat(), Voice.AF22050);
	}

	public static AudioFormat getMP3AudioFormat() {
		return new AudioFormat(new AudioFormat.Encoding("MPEG1L3"), AudioSystem.NOT_SPECIFIED, AudioSystem.NOT_SPECIFIED, 1,
				AudioSystem.NOT_SPECIFIED, AudioSystem.NOT_SPECIFIED, false);
		// endianness doesn't matter
	}

	/**
	 * Determine whether conversion to ogg vorbis format is possible.
	 *
	 * @return AudioSystem.isConversionSupported(getOggAudioFormat(), Voice.AF22050)
	 */
	public static boolean canCreateOgg() {
		return AudioSystem.isConversionSupported(getOggAudioFormat(), Voice.AF22050);
	}

	public static AudioFormat getOggAudioFormat() {
		return new AudioFormat(new AudioFormat.Encoding("VORBIS"), AudioSystem.NOT_SPECIFIED, AudioSystem.NOT_SPECIFIED, 1,
				AudioSystem.NOT_SPECIFIED, AudioSystem.NOT_SPECIFIED, false);
	}

	/**
	 * For an element in a MaryXML document, do what you can to determine the appropriate AllophoneSet. First search for the
	 * suitable voice, then if that fails, go by locale.
	 *
	 * @param e
	 *            e
	 * @return an allophone set if there is any way of determining it, or null.
	 * @throws MaryConfigurationException
	 *             if a suitable allophone set exists in principle, but there were problems loading it.
	 */
	public static AllophoneSet determineAllophoneSet(Element e) throws MaryConfigurationException {
		AllophoneSet allophoneSet = null;
		Element voice = (Element) MaryDomUtils.getAncestor(e, MaryXML.VOICE);
		Voice maryVoice = Voice.getVoice(voice);
		if (maryVoice == null) {
			// Determine Locale in order to use default voice
			Locale locale = MaryUtils.string2locale(e.getOwnerDocument().getDocumentElement().getAttribute("xml:lang"));
			maryVoice = Voice.getDefaultVoice(locale);
		}
		if (maryVoice != null) {
			allophoneSet = maryVoice.getAllophoneSet();
		} else {
			Locale locale = MaryUtils.string2locale(e.getOwnerDocument().getDocumentElement().getAttribute("xml:lang"));
			allophoneSet = determineAllophoneSet(locale);
		}
		return allophoneSet;
	}

	/**
	 * Try to determine the Allophone set to use for the given locale.
	 *
	 * @param locale
	 *            locale
	 * @return the allophone set defined for the given locale, or null if no such allophone set can be determined.
	 * @throws MaryConfigurationException
	 *             if an allophone set exists for the given locale in principle, but there were problems loading it.
	 */
	public static AllophoneSet determineAllophoneSet(Locale locale) throws MaryConfigurationException {
		AllophoneSet allophoneSet = null;
		String propertyPrefix = MaryProperties.localePrefix(locale);
		if (propertyPrefix != null) {
			String propertyName = propertyPrefix + ".allophoneset";
			allophoneSet = needAllophoneSet(propertyName);
		}
		return allophoneSet;
	}

	/**
	 * The mary property setting determining where to save audio data.
	 */
	private static final String audiostoreProperty = MaryProperties.getProperty("synthesis.audiostore", "ram");

	/**
	 * Create an AudioDestination to which the audio data can be written. Depending on the mary property "synthesis.audiostore",
	 * this will use either a ByteArrayOutputStream or a FileOutputStream. The calling code is responsible for administering this
	 * AudioDestination.
	 *
	 * @throws IOException
	 *             if the underlying OutputStream could not be created.
	 * @return AudioDestination(ram)
	 */
	public static AudioDestination createAudioDestination() throws IOException {
		boolean ram = false;
		if (audiostoreProperty.equals("ram"))
			ram = true;
		else if (audiostoreProperty.equals("file"))
			ram = false;
		else // auto
		if (lowMemoryCondition())
			ram = false;
		else
			ram = true;
		return new AudioDestination(ram);
	}

	/**
	 * Convenience method to access the allophone set referenced in the MARY property with the given name.
	 *
	 * @param propertyName
	 *            name of the property referring to the allophone set
	 * @throws MaryConfigurationException
	 *             if the allophone set cannot be obtained
	 * @return the requested allophone set. This method will never return null; if it cannot get the allophone set, it throws an
	 *         exception.
	 */
	public static AllophoneSet needAllophoneSet(String propertyName) throws MaryConfigurationException {
		String propertyValue = MaryProperties.getProperty(propertyName);
		if (propertyValue == null) {
			throw new MaryConfigurationException("No such property: " + propertyName);
		}
		if (AllophoneSet.hasAllophoneSet(propertyValue)) {
			return AllophoneSet.getAllophoneSetById(propertyValue);
		}
		InputStream alloStream;
		try {
			alloStream = MaryProperties.needStream(propertyName);
		} catch (FileNotFoundException e) {
			throw new MaryConfigurationException("Cannot open allophone stream for property " + propertyName, e);
		}
		assert alloStream != null;
		return AllophoneSet.getAllophoneSet(alloStream, propertyValue);
	}

	public static String[] checkLexicon(String propertyName, String token) throws IOException, MaryConfigurationException {
		String lexiconProperty = propertyName + ".lexicon";
		InputStream lexiconStream = MaryProperties.needStream(lexiconProperty);
		FSTLookup lexicon = new FSTLookup(lexiconStream, lexiconProperty);
		return lexicon.lookup(token.toLowerCase());
	}

	public static String getMaryVersion() {
		String output = "Mary TTS server " + Version.specificationVersion() + " (impl. " + Version.implementationVersion() + ")";

		return output;
	}

	public static String getDataTypes() {
		String output = "";

		List<MaryDataType> allTypes = MaryDataType.getDataTypes();

		for (MaryDataType t : allTypes) {
			output += t.name();
			if (t.isInputType())
				output += " INPUT";
			if (t.isOutputType())
				output += " OUTPUT";

			output += System.getProperty("line.separator");
		}

		return output;
	}

	public static String getLocales() {
		StringBuilder out = new StringBuilder();
		for (LanguageConfig conf : MaryConfig.getLanguageConfigs()) {
			for (Locale locale : conf.getLocales()) {
				out.append(locale).append('\n');
			}
		}
		return out.toString();
	}

	public static String getVoices() {
		String output = "";
		Collection<Voice> voices = Voice.getAvailableVoices();
		for (Iterator<Voice> it = voices.iterator(); it.hasNext();) {
			Voice v = (Voice) it.next();
			if (v instanceof InterpolatingVoice) {
				// do not list interpolating voice
			} else if (v instanceof UnitSelectionVoice) {
				output += v.getName() + " " + v.getLocale() + " " + v.gender().toString() + " " + "unitselection" + " "
						+ ((UnitSelectionVoice) v).getDomain() + System.getProperty("line.separator");
			} else if (v instanceof HMMVoice) {
				output += v.getName() + " " + v.getLocale() + " " + v.gender().toString() + " " + "hmm"
						+ System.getProperty("line.separator");
			} else {
				output += v.getName() + " " + v.getLocale() + " " + v.gender().toString() + " " + "other"
						+ System.getProperty("line.separator");
			}
		}

		return output;
	}

	public static String getDefaultVoiceName() {
		String defaultVoiceName = "";
		String allVoices = getVoices();
		if (allVoices != null && allVoices.length() > 0) {
			StringTokenizer tt = new StringTokenizer(allVoices, System.getProperty("line.separator"));
			if (tt.hasMoreTokens()) {
				defaultVoiceName = tt.nextToken();
				StringTokenizer tt2 = new StringTokenizer(defaultVoiceName, " ");
				if (tt2.hasMoreTokens())
					defaultVoiceName = tt2.nextToken();
			}
		}

		return defaultVoiceName;
	}

	public static String getExampleText(String datatype, Locale locale) {
		MaryDataType type = MaryDataType.get(datatype);
		String exampleText = type.exampleText(locale);
		if (exampleText != null)
			return exampleText.trim() + System.getProperty("line.separator");
		return "";
	}

	public static Vector<String> getDefaultVoiceExampleTexts() {
		String defaultVoiceName = getDefaultVoiceName();
		Vector<String> defaultVoiceExampleTexts = null;
		defaultVoiceExampleTexts = StringUtils.processVoiceExampleText(getVoiceExampleText(defaultVoiceName));
		if (defaultVoiceExampleTexts == null) // Try for general domain
		{
			String str = getExampleText("TEXT", Voice.getVoice(defaultVoiceName).getLocale());
			if (str != null && str.length() > 0) {
				defaultVoiceExampleTexts = new Vector<String>();
				defaultVoiceExampleTexts.add(str);
			}
		}

		return defaultVoiceExampleTexts;
	}

	public static String getVoiceExampleText(String voiceName) {
		Voice v = Voice.getVoice(voiceName);
		if (v instanceof marytts.unitselection.UnitSelectionVoice)
			return ((marytts.unitselection.UnitSelectionVoice) v).getExampleText();
		return "";
	}

	/**
	 * For the voice with the given name, return the list of vocalizations supported by this voice, one vocalization per line.
	 * These values can be used in the "name" attribute of the vocalization tag.
	 *
	 * @param voiceName
	 *            voiceName
	 * @return the list of vocalizations, or the empty string if the voice does not support vocalizations.
	 */
	public static String getVocalizations(String voiceName) {
		Voice v = Voice.getVoice(voiceName);
		if (v == null || !v.hasVocalizationSupport()) {
			return "";
		}
		VocalizationSynthesizer vs = v.getVocalizationSynthesizer();
		assert vs != null;
		String[] vocalizations = vs.listAvailableVocalizations();
		assert vocalizations != null;
		return StringUtils.toString(vocalizations);
	}

	/**
	 * For the voice with the given name, return the list of styles supported by this voice, if any, one style per line. These
	 * values can be used as the global "style" value in a synthesis request, or in the "style" attribute of the MaryXML prosody
	 * element.
	 *
	 * @param voiceName
	 *            voiceName
	 * @return the list of styles, or the empty string if the voice does not support styles.
	 */
	public static String getStyles(String voiceName) {
		Voice v = Voice.getVoice(voiceName);
		String[] styles = null;
		if (v != null) {
			styles = v.getStyles();
		}
		if (styles != null) {
			return StringUtils.toString(styles);
		}
		return "";
	}

	public static String getDefaultAudioEffects() {
		// Marc, 8.1.09: Simplified format
		// name params
		StringBuilder sb = new StringBuilder();
		for (AudioEffect effect : AudioEffects.getEffects()) {
			sb.append(effect.getName()).append(" ").append(effect.getExampleParameters()).append("\n");
		}
		return sb.toString();
	}

	public static String getAudioEffectDefaultParam(String effectName) {
		AudioEffect effect = AudioEffects.getEffect(effectName);
		if (effect == null) {
			return "";
		}
		return effect.getExampleParameters().trim();
	}

	public static String getFullAudioEffect(String effectName, String currentEffectParams) {
		AudioEffect effect = AudioEffects.getEffect(effectName);
		if (effect == null) {
			return "";
		}
		effect.setParams(currentEffectParams);
		return effect.getFullEffectAsString();
	}

	public static String getAudioEffectHelpText(String effectName) {
		AudioEffect effect = AudioEffects.getEffect(effectName);
		if (effect == null) {
			return "";
		}
		return effect.getHelpText().trim();
	}

	public static String isHmmAudioEffect(String effectName) {
		AudioEffect effect = AudioEffects.getEffect(effectName);
		if (effect == null) {
			return "";
		}
		return effect.isHMMEffect() ? "yes" : "no";
	}

}
